第 3 课:ReAct 架构(Reason + Act)
在上一课的 Decision Engine 中,我们其实已经给出一套很好的容错代码。
如果你仔细观察会发现里面存在两个 循环:
-
第一个循环是处理
提问得到的回答和要求不符/存在错误,当前策略是把当前错误信息添加到历史记录中,并重新提问。超过最大尝试次数后终止提问,并给出保守回答。
在 函数
decide_with_retry中:for attempt in range(max_retries + 1): # 在有限的循环次数内,添加错误信息并尝试 # 如果上次发送得到的回答 经过检查存在错误,本次对话中加上上次的报错信息。 if err_msg: messages.append({ "role": "system", "content": f"Your previous output failed validation: {err_msg}. " f"Output ONLY one valid JSON object that matches the schema." }) # 获取回答 raw = call_llm(messages) # 错误类型一:回答内容str 不能严格按照 json 格式 try: obj = json.loads(raw) # 把 str 转化成 dict except Exception as e: err_msg = f"Invalid JSON parse error: {type(e).__name__}" continue # 错误类型二:回答内容存在逻辑错误,使用之前定义的函数检测 ok, reason = validate_decision(obj, tools_available) if ok: # 如果成功,则返回回答的结果 dict。包含action,tool,tool_input, final return obj else: # 如果失败,添加错误原因到 对话中 err_msg = reason # 指数退避(简单版) time.sleep(0.4 * (2 ** attempt)) -
第二个循环是处理
工具调用失败的问题,但是其实没有用 for 循环写出来当前策略是把工具调用的错误信息写入
工具的调用结果 Observation中后, 在函数decide_with_retry中添加到聊天记录中,然后重新提问一次(没有循环尝试)。相当于只允许一次工具调用失败,其实也可以改成最大允许失败次数。if decision1["action"] == "tool_call": # 如果第一次得到的回答是”要用工具 tool = decision1["tool"] # 获取工具的名字 tool_input = decision1["tool_input"]# 获取工具需要的输入 # 获取工具执行的 返回(也就是 obeservation) try: if tool == "search": query = tool_input.get("query", "") obs = TOOLS["search"](query) else: raise RuntimeError("Unknown tool") except Exception as e: obs = f"[TOOL_ERROR] {type(e).__name__}: {e}" # 第二步:把 Observation 注入,要求模型基于 observation 输出 final decision2 = decide_with_retry( state={"goal": goal, "note": "Use the observation to answer."}, #注明使用 observation 回答 tools_available=tools_available, last_observation=obs ) print("Decision2:", json.dumps(decision2, ensure_ascii=False)) else: # 不需要工具,直接 final print("Final:", decision1["final"])
(一)ReAct 结构
这种检查容错机制 其实就是本节课要讲的 ReAct 架构,能够处理 模型回答 和 工具调用的失败,不会死循环。
ReAct = Reason(推理) + Act(行动) + Observation(观察)
也就是说:
- 每一步让 LLM 按照模板给出决策(Reason),并检查
- 按照 LLM 的决策调用工具(Act),获取反馈(Observation)
- 把 Observation 放入下一轮对话的记忆中。直到 finish
[State] → LLM(Decision) → [Action JSON]
↓
Validator / Guardrails
↓
Tool Executor / Error Handler
↓
Observation
↓
Update State
↺
(二)容错机制
1. 模型返回检查(Validator)
raw = call_llm(...)
obj = json.loads(raw)
validate_decision(obj)
Validator 至少做三层校验:
- 结构层:字段是否齐全
- 类型层:
isinstance(...) - 语义层:
- action 是否允许
- tool 是否存在
- action 与字段是否一致
2. 工具调用检查
工具失败 ≠ Agent 崩溃 工具失败 = 新的 Observation
try:
obs = tool(...)
except Exception as e:
obs = f"[TOOL_ERROR] {type(e).__name__}: {e}"
然后:
- 把这个 obs 作为 Observation 喂回模型
- 让模型决定:
- 换工具
- replan
- ask_user
- finish(降级)
千万不要做的事 ❌
- 工具异常直接
raise - 不告诉模型失败原因
- 悄悄重试无限次
3. 如何避免 Agent 无限循环
你至少要做 4 层防护:
- max_steps(硬上限)
for step in range(max_steps):
-
动作约束(不能乱来)
-
不允许 tool_call 无限重复
-
连续 replan 次数限制
-
-
终止状态变化检测
if state == last_state:
force_replan()
- 指数退避(你刚学过)
(三)思维链被截断的RePlan
有时候会出现这种典型表现:
- 连续 tool_call 但结果无用
- 重复同一个 action
- 输出越来越短 / 空
- Validator 连续失败
因此 ReAct 必须具备的“自救能力”。(Re-evaluate / Replan)
策略 1:显式 replan 动作
你已经允许了:
{"action": "replan"}
在 replan 时:
-
清空 observation
-
更新 state:
state["note"] = "Previous approach failed. Try a new plan."
策略 2:强制 meta 提示
在 system 里加一条:
If you are stuck or repeating actions, choose "replan".
策略 3:外部强制切断
if repeated_actions > 2:
return "Unable to proceed. Please clarify the goal."
(四)工具 schema
如果你仔细观察,会发现
现在这份代码里: 👉 并没有任何地方“规定”
tool_search的输入参数必须叫query👉 模型之所以输出了{"query": ...},完全是“猜的 / 习惯性的 / 概率性行为” 👉 这在工程上是 ❌ 不安全、❌ 不可控、❌ 迟早出 bug 的
因此需要明确声明工具 schema(给模型看的)
1. 提示词模板
TOOL_SCHEMAS = {
"search": {
"description": "Search the web for information",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search keywords"
}
},
"required": ["query"]
}
}
}
把 schema 注入到 prompt(关键)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "system", "content": f"Tool schemas:\n{json.dumps(TOOL_SCHEMAS)}"},
{"role": "user", "content": json.dumps({"state": state})}
]
这样模型才知道:
- search 这个工具
- 必须提供
query query是 string- 少了就不合法